﻿//-----------------------------------------------------------------------
// <copyright>
// Copyright (C) Sergey Solyanik for The Black Square Project.
//
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//----------------------------------------------------------------------- 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;

using Microsoft.Win32;

using CommonUtils;
using DataModel;
using Notifier.com.microsoft.mail;

namespace Notifier
{
    /// <summary>
    /// Sends an email every time a review is requested or performed.
    /// Must be executed from a real user context.
    /// </summary>
    class Program
    {
        /// <summary>
        /// Holds registry configuration for the mailer.
        /// </summary>
        private class Configuration
        {
            /// <summary>
            /// Base configuration key.
            /// </summary>
            private const string REGISTRY_KEY = "software\\Microsoft\\BlackSquare";

            /// <summary>
            /// For now (to be customized later) the webroot.
            /// </summary>
            public const string WEBROOT = "BlackSquare";

            /// <summary>
            /// User name.
            /// E.g. alice
            /// </summary>
            public string user;

            /// <summary>
            /// Password.
            /// </summary>
            public string password;

            /// <summary>
            /// User domain (as in DOMAIN\user).
            /// E.g. REDMOND
            /// </summary>
            public string domain;

            /// <summary>
            /// User account (without email domain) from which to send.
            /// E.g. bob
            /// </summary>
            public string fromemail;

            /// <summary>
            /// The database instance.
            /// E.g. localhost\mysqlinstance
            /// </summary>
            public string database;

            /// <summary>
            /// Web server where BlackSquare is hosted.
            /// E.g. sergeydev1
            /// </summary>
            public string webserver;

            /// <summary>
            /// If using Exchange, the URL of email service. Otherwise null.
            /// E.g. https://mail.microsoft.com/EWS/Exchange.asmx
            /// </summary>
            public string emailservice;

            /// <summary>
            /// If using SMTP server, its hostname.
            /// E.g. smtp.redmond.microsoft.com
            /// </summary>
            public string smtpserver;

            /// <summary>
            /// Whether to use SSL with the smtp service. Only used for SMTP transport.
            /// </summary>
            public bool useSsl;

            /// <summary>
            /// Whether to use ActiveDirectory to resolve email addresses.
            /// </summary>
            public bool useLdap;

            /// <summary>
            /// The email domain
            /// </summary>
            public string emaildomain;

            /// <summary>
            /// Verifies that various pieces are either missing, or correctly formatted.
            /// </summary>
            /// <returns></returns>
            private bool VerifyParts()
            {
                bool result = true;
                if ((user != null) && (user.Contains('@') || user.Contains('\\')))
                {
                    Console.WriteLine("User name should not contain the domain information. E.g.: bob");
                    result = false;
                }
                if ((domain != null) && (domain.Contains('.') || domain.Contains('@') || domain.Contains('\\')))
                {
                    Console.WriteLine("Domain name should be unqualified netbios domain. E.g.: REDMOND");
                    result = false;
                }
                if ((fromemail != null) && (fromemail.Contains('@') || fromemail.Contains('\\')))
                {
                    Console.WriteLine("'From' user name should not contain the domain information. E.g.: alice");
                    result = false;
                }
                if ((emailservice != null) &&
                    !(emailservice.StartsWith("http", StringComparison.InvariantCultureIgnoreCase) &&
                    emailservice.EndsWith("asmx", StringComparison.InvariantCultureIgnoreCase)))
                {
                    Console.WriteLine("Exchange service does not seem to be configured correctly.");
                    Console.WriteLine("Expecting something like: https://mail.microsoft.com/EWS/Exchange.asmx");
                    result = false;
                }
                if ((smtpserver != null) && ((smtpserver.Contains('@') || smtpserver.Contains('\\') ||
                    smtpserver.Contains('/'))))
                {
                    Console.WriteLine("SMTP server hostname contains incorrect characters.");
                    Console.WriteLine("Expecting something like: smtp.redmond.microsoft.com");
                    result = false;
                }
                if ((emaildomain != null) && (emaildomain.Contains('@') || emaildomain.Contains('\\') ||
                    emaildomain.Contains('/')))
                {
                    Console.WriteLine("Email domain contains incorrect characters.");
                    Console.WriteLine("Expecting something like: microsoft.com");
                    result = false;
                }

                return result;
            }

            /// <summary>
            /// Verifies that the configuration is in a ready to run state.
            /// </summary>
            private bool VerifyWhole()
            {
                if (!VerifyParts())
                    return false;

                bool result = true;
                if (smtpserver != null && emailservice != null)
                {
                    Console.WriteLine("Can only have either SMTP or Exchange configured.");
                    Console.WriteLine("Please reset the configuration and try again.");
                    result = false;
                }

                if ((smtpserver == null && emailservice == null) || user == null || database == null ||
                    webserver == null || emaildomain == null)
                {
                    Console.WriteLine("You need to configure user credentials, mail server, web server, " +
                        "and database connection string first.");
                    result = false;
                }

                if (emailservice != null && (password == null || domain == null))
                {
                    Console.WriteLine("User credentials are required for Exchange service.");
                    result = false;
                }

                return result;
            }

            /// <summary>
            /// Trivial constructor. Does nothing.
            /// </summary>
            public Configuration()
            {
            }

            /// <summary>
            /// Saves the partial configuration into registry.
            /// </summary>
            public void Save()
            {
                if (!VerifyParts())
                    return;

                RegistryKey key = Registry.CurrentUser.CreateSubKey(REGISTRY_KEY);

                if (user != null)
                {
                    key.SetValue("user", user);
                    if (password != null)
                        key.SetValue("password", Convert.ToBase64String(
                            ProtectedData.Protect(Encoding.UTF8.GetBytes(password), null,
                            DataProtectionScope.CurrentUser)));
                    else
                        key.DeleteValue("password", false);

                    if (domain != null)
                        key.SetValue("domain", domain);
                    else
                        key.DeleteValue("domain", false);

                    if (fromemail != null)
                        key.SetValue("fromemail", fromemail);
                    else
                        key.DeleteValue("fromemail", false);
                }

                if (emailservice != null)
                {
                    if (key.GetValue("smtpserver") != null)
                    {
                        Console.WriteLine("SMTP server is already configured.");
                        Console.WriteLine("Please reset the configuration to switch to Exchange!");
                    }
                    else
                    {
                        key.SetValue("emailservice", emailservice);
                        key.SetValue("useldap", useLdap.ToString());
                    }
                }

                if (smtpserver != null)
                {
                    if (key.GetValue("emailservice") != null)
                    {
                        Console.WriteLine("Exchange service is already configured.");
                        Console.WriteLine("Please reset the configuration to switch to SMTP!");
                    }
                    else
                    {
                        key.SetValue("smtpserver", smtpserver);
                        key.SetValue("usessl", useSsl.ToString());
                        key.SetValue("useldap", useLdap.ToString());
                    }
                }

                if (emaildomain != null)
                    key.SetValue("emaildomain", emaildomain);

                if (database != null)
                    key.SetValue("database", database);

                if (webserver != null)
                    key.SetValue("webserver", webserver);

                key.Close();
            }

            /// <summary>
            /// Loads the configuration.
            /// </summary>
            /// <returns></returns>
            public static Configuration Load()
            {
                Configuration cfg = new Configuration();

                RegistryKey key = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY);
                if (key == null)
                {
                    Console.WriteLine("You need to configure service parameters first.");
                    return null;
                }

                try
                {
                    cfg.user = (string)key.GetValue("user");
                    cfg.password = (string)key.GetValue("password");
                    cfg.domain = (string)key.GetValue("domain");
                    cfg.fromemail = (string)key.GetValue("fromemail");
                    cfg.database = (string)key.GetValue("database");
                    cfg.webserver = (string)key.GetValue("webserver");
                    cfg.emaildomain = (string)key.GetValue("emaildomain");
                    cfg.emailservice = (string)key.GetValue("emailservice");
                    cfg.smtpserver = (string)key.GetValue("smtpserver");

                    string useSslStr = (string)key.GetValue("usessl");
                    bool useSsl = false;
                    if (useSslStr != null && bool.TryParse(useSslStr, out useSsl))
                        cfg.useSsl = useSsl;

                    string useLdapStr = (string)key.GetValue("useldap");
                    bool useLdap = false;
                    if (useLdapStr != null && bool.TryParse(useLdapStr, out useLdap))
                        cfg.useLdap = useLdap;
                }
                finally
                {
                    key.Close();
                }

                if (!cfg.VerifyWhole())
                    return null;

                if (cfg.password != null)
                    cfg.password = Encoding.UTF8.GetString(
                        ProtectedData.Unprotect(
                        Convert.FromBase64String(cfg.password), null, DataProtectionScope.CurrentUser));

                return cfg;
            }

            /// <summary>
            /// Resets the configuration.
            /// </summary>
            public static void Reset()
            {
                RegistryKey key = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY, true);
                if (key == null)
                    return;

                key.DeleteValue("user", false);
                key.DeleteValue("password", false);
                key.DeleteValue("domain", false);
                key.DeleteValue("fromemail", false);
                key.DeleteValue("database", false);
                key.DeleteValue("webserver", false);
                key.DeleteValue("emaildomain", false);
                key.DeleteValue("emailservice", false);
                key.DeleteValue("smtpserver", false);
                key.DeleteValue("usessl", false);
                key.DeleteValue("useldap", false);
            }
        }

        /// <summary>
        /// Name of the scheduled task.
        /// </summary>
        private const string SchedulTaskName = "BlackSquareNotifier";

        /// <summary>
        /// For ldap queries, a dictionary that hashes the user name to an email address.
        /// </summary>
        private static Dictionary<string, string> emailDictionary;

        /// <summary>
        /// For ldap queries, a directory searcher object.
        /// </summary>
        private static DirectorySearcher directorySearcher;

        /// <summary>
        /// Displays the help string.
        /// </summary>
        private static void DisplayUsage()
        {
            Console.WriteLine("To configure for Exchange:");
            Console.WriteLine("    Notifier exchange webserviceurl companydomain [useldap]");
            Console.WriteLine("To configure for smtp:");
            Console.WriteLine("    Notifier smtp smtpserver companydomain [usessl] [useldap]");
            Console.WriteLine("Common configuration:");
            Console.WriteLine("    Notifier credentials username [password domain] [aliastosendfrom]");
            Console.WriteLine("    Notifier webserver webservername");
            Console.WriteLine("    Notifier database SQL_server_instance");
            Console.WriteLine("    Notifier schedule repetition_interval_in_minutes");
            Console.WriteLine("To reset configuration:");
            Console.WriteLine("    Notifier reset");
            Console.WriteLine("To use (only after configuration!):");
            Console.WriteLine("    Notifier");
            Console.WriteLine("Or, more likely, create a scheduled task that runs Notifier every");
            Console.WriteLine("10 minutes by running \"Notifier schedule 10\"");
            Console.WriteLine();
            Console.WriteLine("To send a reminder to attend to older code reviews (no activity for number_of_days):");
            Console.WriteLine("    Notifier --remind number_of_days");
            Console.WriteLine();
            Console.WriteLine("All configuration steps must be completed before the program is started.");
            Console.WriteLine();
            Console.WriteLine("Use either SMTP of Exchange configuration, not both. Always reset the");
            Console.WriteLine("configuration when switching between SMTP and Exchange.");
            Console.WriteLine();
            Console.WriteLine("If Exchange mode is used, the web service url should point to Exchange");
            Console.WriteLine("Web Server 2007. The format of the string should look like this:");
            Console.WriteLine("        https://owa.microsoft.com/EWS/Exchange.asmx");
            Console.WriteLine();
            Console.WriteLine("User password and domain are required for the Exchange server,");
            Console.WriteLine("but are optional for SMTP.");
            Console.WriteLine();
            Console.WriteLine("Note: a 'send of behalf' address can be specified. If it is, the mail will");
            Console.WriteLine("come from that account. Note that the logon account must have rights to send");
            Console.WriteLine("on behalf of this (optional) email alias. The domain of this alias is the");
            Console.WriteLine("same as the user account.");
            Console.WriteLine();
            Console.WriteLine("To test the setup, run 'Notifier testsetup email' where email");
            Console.WriteLine("is the alias (without the domain name to which the test email will be sent.");
        }

        /// <summary>
        /// Stores credentials in registry.
        /// </summary>
        /// <param name="args"> The argument array, presumed to be one of:
        ///     {"credentials", username, password, domain}
        ///     {"credentials", username, password, domain, user name to send on behalf of}
        ///     {"credentials", username, user name to send on behalf of}
        ///     
        /// optionally email on behalf of which to send} </param>
        private static void SetCredentials(string[] args)
        {
            if (args.Length < 2 || args.Length > 5)
            {
                Console.WriteLine("Expected a different set of parameters. Use 'help' for help.");
                return;
            }

            Configuration cfg = new Configuration();
            cfg.user = args[1];
            if (args.Length == 3)
            {
                cfg.fromemail = args[2];
            }
            else if (args.Length > 3)
            {
                cfg.password = args[2];
                cfg.domain = args[3];
                if (args.Length == 5)
                    cfg.fromemail = args[4];
            }
            cfg.Save();
        }

        /// <summary>
        /// Stores exchange information in registry.
        /// </summary>
        /// <param name="args"> The argument array, presumed to be {"exchange", EWS 2007 url, domain [, useldap]} </param>
        private static void SetExchangeServer(string[] args)
        {
            if (args.Length != 3 && args.Length != 4)
            {
                Console.WriteLine("Need URL and email domain");
                return;
            }

            if (args.Length == 4 && !"useldap".Equals(args[3], StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("The last argument can only be useldap.");
                return;
            }

            Configuration cfg = new Configuration();
            cfg.emailservice = args[1];
            cfg.emaildomain = args[2];
            if (args.Length == 4)
                cfg.useLdap = true;

            cfg.Save();
        }

        /// <summary>
        /// Stores SMTP information in registry.
        /// </summary>
        /// <param name="args"> The argument array, presumed to be {"smtp", smtp server, domain [, usessl] [, useldap]} </param>
        private static void SetSmtpServer(string[] args)
        {
            if (args.Length < 3 || args.Length > 5)
            {
                Console.WriteLine("Need hostname and email domain");
                return;
            }

            Configuration cfg = new Configuration();
            cfg.smtpserver = args[1];
            cfg.emaildomain = args[2];

            for (int i = 3; i < args.Length; ++i)
            {
                if ("useLdap".Equals(args[i], StringComparison.OrdinalIgnoreCase))
                {
                    cfg.useLdap = true;
                }
                else if ("useSsl".Equals(args[i], StringComparison.OrdinalIgnoreCase))
                {
                    cfg.useSsl = true;
                }
                else
                {
                    Console.WriteLine("{0} is not recognized!", args[i]);
                    return;
                }
            }
            cfg.Save();
        }

        /// <summary>
        /// Stores database connection string in registry.
        /// </summary>
        /// <param name="args"> The argument array, presumed to be {"database", connectionString} </param>
        private static void SetDatabase(string[] args)
        {
            if (args.Length != 2)
            {
                Console.WriteLine("Need database connection string");
                return;
            }

            Configuration cfg = new Configuration();
            cfg.database = args[1];
            cfg.Save();
        }

        /// <summary>
        /// Stores web server name in registry.
        /// </summary>
        /// <param name="args"> The argument array, presumed to be {"webserver", connectionString} </param>
        private static void SetWebserver(string[] args)
        {
            if (args.Length != 2)
            {
                Console.WriteLine("Need web server name");
                return;
            }

            Configuration cfg = new Configuration();
            cfg.webserver = args[1];
            cfg.Save();
        }

        /// <summary>
        /// Add, update or delete notifier as repetition task in Windows Task Scheduler.
        /// </summary>
        /// <param name="args"> The argument array, presumed to be {"schedule", interval_in_minutes} </param>
        private static void SetScheduleTask(string[] args)
        {
            int interval;
            if (args.Length != 2 || !Int32.TryParse(args[1], out interval))
            {
                Console.WriteLine("Notifier schedule repetition_interval_in_minutes");
                Console.WriteLine("Set the repetition interval to a positive integar to add/update a scheduled task.");
                Console.WriteLine("Set the repetition interval to 0 to delete the existing task.");
                return;
            }

            TaskScheduler taskScheduler = new TaskScheduler();
            taskScheduler.Interval = interval;
            taskScheduler.TaskName = SchedulTaskName;
            taskScheduler.TaskPath = Process.GetCurrentProcess().MainModule.FileName;

            Configuration cfg = Configuration.Load();
            if (cfg == null)
            {
                Console.WriteLine("Please configure service parameters before echeduling the task!");
                return;
            }

            bool success = taskScheduler.SetTask(cfg.user, cfg.password);
            if (success)
                Console.WriteLine("Created/updated/deleted the scheduled task successfully. (repetition = {0} min)",
                    taskScheduler.Interval);
            else
                Console.WriteLine("Failed to set schedule task, please create the task manually.");
        }

        /// <summary>
        /// Processes configuration parameters.
        /// </summary>
        /// <param name="args"></param>
        private static void SetConfiguration(string[] args)
        {
            if (args[0].Equals("reset"))
            {
                Configuration.Reset();
                return;
            }

            if (args[0].Equals("credentials", StringComparison.InvariantCultureIgnoreCase))
            {
                SetCredentials(args);
                return;
            }

            if (args[0].Equals("exchange", StringComparison.InvariantCultureIgnoreCase))
            {
                SetExchangeServer(args);
                return;
            }

            if (args[0].Equals("smtp", StringComparison.InvariantCultureIgnoreCase))
            {
                SetSmtpServer(args);
                return;
            }

            if (args[0].Equals("webserver", StringComparison.InvariantCultureIgnoreCase))
            {
                SetWebserver(args);
                return;
            }

            if (args[0].Equals("schedule", StringComparison.InvariantCultureIgnoreCase))
            {
                SetScheduleTask(args);
                return;
            }

            if (args[0].Equals("database", StringComparison.InvariantCultureIgnoreCase))
            {
                SetDatabase(args);
                return;
            }

            DisplayUsage();
        }

        /// <summary>
        /// Converts the review status to a verdict sentence.
        /// </summary>
        /// <param name="status"> The numeric code for the verdict. </param>
        /// <returns></returns>
        private static string ReviewStatusToSentence(int verdict)
        {
            switch (verdict)
            {
                case 0: return "I think this document needs more work before it is submitted.";
                case 1: return "This looks good, but I do recommend a few minor tweaks.";
                case 2: return "LGTM.";
            }

            return "I've made a few comments, but they are non-scoring :-).";
        }

        /// <summary>
        /// Creates and populated Exchange message type.
        /// </summary>
        /// <param name="to"> Email address to send to. </param>
        /// <param name="from"> Email address of send on behalf, or null. </param>
        /// <param name="replyTo"> An alias of the person on behalf of which the mail is sent. </param>
        /// <param name="subject"> Subject. </param>
        /// <param name="body"> Body of the message. </param>
        /// <returns> Created message structure. </returns>
        private static MessageType MakeExchangeMessage(string to, string from, string replyTo, string subject,
            string body)
        {
            MessageType message = new MessageType();

            EmailAddressType recipient = new EmailAddressType();
            recipient.EmailAddress = to;

            message.ToRecipients = new EmailAddressType[1];
            message.ToRecipients[0] = recipient;

            if (replyTo != null)
            {
                EmailAddressType reply = new EmailAddressType();
                reply.EmailAddress = replyTo;

                message.ReplyTo = new EmailAddressType[1];
                message.ReplyTo[0] = reply;
            }

            message.Subject = subject;
            message.Sensitivity = SensitivityChoicesType.Normal;

            if (from != null)
            {
                message.From = new SingleRecipientType();
                message.From.Item = new EmailAddressType();
                message.From.Item.EmailAddress = from;
            }

            message.Body = new BodyType();
            message.Body.BodyType1 = BodyTypeType.Text;
            message.Body.Value = body;

            return message;
        }

        /// <summary>
        /// Creates and populated Exchange message type.
        /// </summary>
        /// <param name="to"> List of 'to' addresses. </param>
        /// <param name="cc"> List of 'cc' addresses. </param>
        /// <param name="from"> On-behalf-of account, or null. </param>
        /// <param name="replyTo"> An alias of the person on behalf of which the mail is sent. </param>
        /// <param name="subject"> Subject. </param>
        /// <param name="body"> Body. </param>
        /// <returns> Created message structure. </returns>
        private static MessageType MakeExchangeMessage(List<string> to, List<string> cc, string from, string replyTo,
            string subject, string body)
        {
            MessageType message = new MessageType();

            List<EmailAddressType> recipients = new List<EmailAddressType>();
            foreach (string email in to)
            {
                EmailAddressType address = new EmailAddressType();
                address.EmailAddress = email;
                recipients.Add(address);
            }
            message.ToRecipients = recipients.ToArray();

            recipients = new List<EmailAddressType>();
            foreach (string email in cc)
            {
                EmailAddressType address = new EmailAddressType();
                address.EmailAddress = email;
                recipients.Add(address);
            }
            message.CcRecipients = recipients.ToArray();

            if (from != null)
            {
                message.From = new SingleRecipientType();
                message.From.Item = new EmailAddressType();
                message.From.Item.EmailAddress = from;
            }

            if (replyTo != null)
            {
                EmailAddressType reply = new EmailAddressType();
                reply.EmailAddress = replyTo;

                message.ReplyTo = new EmailAddressType[1];
                message.ReplyTo[0] = reply;
            }

            message.Subject = subject;
            message.Sensitivity = SensitivityChoicesType.Normal;

            message.Body = new BodyType();
            message.Body.BodyType1 = BodyTypeType.Text;

            message.Body.Value = body;

            return message;
        }

        /// <summary>
        /// Creates System.Net.Mail.MailMessage.
        /// </summary>
        /// <param name="to"> Email to send to. </param>
        /// <param name="from"> On behalf email, or null. </param>
        /// <param name="replyTo"> An alias of the person on behalf of which the mail is sent. </param>
        /// <param name="sender"> Email of a sender. </param>
        /// <param name="subject"> Subject. </param>
        /// <param name="body"> Body. </param>
        /// <returns> Created message structure. </returns>
        private static MailMessage MakeSmtpMessage(string to, string from, string replyTo, string sender,
            string subject, string body)
        {
            if (from == null)
                from = sender;

            MailMessage message = new MailMessage();
            message.To.Add(to);
            message.Subject = subject;
            message.From = new MailAddress(from);
            message.Sender = new MailAddress(from);

            if (replyTo != null)
                message.ReplyTo = new MailAddress(replyTo);

            message.Body = body;
            message.IsBodyHtml = false;

            return message;
        }

        /// <summary>
        /// Creates System.Net.Mail.MailMessage.
        /// </summary>
        /// <param name="to"> List of emails for the 'to' line. </param>
        /// <param name="cc"> List of emails for 'cc' line. </param>
        /// <param name="from"> On behalf email, or null. </param>
        /// <param name="replyTo"> An alias of the person on behalf of which the mail is sent. </param>
        /// <param name="sender"> Email of a sender. </param>
        /// <param name="subject"> Subject. </param>
        /// <param name="body"> Body. </param>
        /// <returns> Created message structure. </returns>
        private static MailMessage MakeSmtpMessage(List<string> to, List<string> cc, string from, string replyTo,
            string sender, string subject, string body)
        {
            if (from == null)
                from = sender;

            MailMessage message = new MailMessage();
            foreach (string address in to)
                message.To.Add(address);
            foreach (string address in cc)
                message.CC.Add(address);

            if (replyTo != null)
                message.ReplyTo = new MailAddress(replyTo);

            message.Subject = subject;
            message.From = new MailAddress(from);
            message.Sender = new MailAddress(from);
            message.Body = body;
            message.IsBodyHtml = false;

            return message;
        }

        /// <summary>
        /// Sends mail through the exchange server.
        /// </summary>
        /// <param name="cfg"> Configuration. </param>
        /// <param name="exchangeItems"> Mail to send. </param>
        /// <returns> true if successful. </returns>
        private static bool SendExchangeMail(Configuration cfg, List<MessageType> exchangeItems)
        {
            ExchangeServiceBinding binding = new ExchangeServiceBinding();
            binding.Credentials = new NetworkCredential(cfg.user, cfg.password, cfg.domain);
            binding.Url = cfg.emailservice;

            DistinguishedFolderIdType folder = new DistinguishedFolderIdType();
            folder.Id = DistinguishedFolderIdNameType.sentitems;

            TargetFolderIdType targetFolder = new TargetFolderIdType();
            targetFolder.Item = folder;

            CreateItemType createItem = new CreateItemType();
            createItem.MessageDisposition = MessageDispositionType.SendAndSaveCopy;
            createItem.MessageDispositionSpecified = true;
            createItem.SavedItemFolderId = targetFolder;

            createItem.Items = new NonEmptyArrayOfAllItemsType();
            createItem.Items.Items = exchangeItems.ToArray();

            CreateItemResponseType response = binding.CreateItem(createItem);

            bool result = true;
            foreach (ResponseMessageType r in response.ResponseMessages.Items)
            {
                if (r.ResponseClass != ResponseClassType.Success)
                {
                    Console.WriteLine("Failed to send the message. ");
                    Console.WriteLine(r.MessageText);

                    result = false;
                }
            }

            return result;
        }

        /// <summary>
        /// Sends mail through SMTP server.
        /// </summary>
        /// <param name="cfg"> Configuration. </param>
        /// <param name="smtpItems"> Mail to send. </param>
        /// <returns> true if successful. </returns>
        private static bool SendSmtpMail(Configuration cfg, List<MailMessage> smtpItems)
        {
            SmtpClient client = new SmtpClient(cfg.smtpserver);
            if (cfg.password == null)
                client.UseDefaultCredentials = true;
            else
                client.Credentials = new NetworkCredential(cfg.user, cfg.password, cfg.domain);

            if (cfg.useSsl)
                client.EnableSsl = true;

            foreach (MailMessage email in smtpItems)
                client.Send(email);

            return true;
        }

        /// <summary>
        /// Resolves the email address from the user name, performing LDAP query if necessary.
        /// </summary>
        /// <param name="cfg"></param>
        /// <param name="userName"></param>
        /// <returns></returns>
        private static string ResolveUser(Configuration cfg, string userName)
        {
            if (!cfg.useLdap)
                return userName + "@" + cfg.emaildomain;

            if (emailDictionary == null)
            {
                emailDictionary = new Dictionary<string, string>();
            }

            string email = emailDictionary.ContainsKey(userName) ? emailDictionary[userName] : null;
            if (email != null)
                return email;

            if (directorySearcher == null)
            {
                directorySearcher = new DirectorySearcher();
                directorySearcher.PropertiesToLoad.Add("mail");
            }

            directorySearcher.Filter = "(SAMAccountName=" + userName + ")";
            SearchResult result = directorySearcher.FindOne();
            if (result != null)
            {
                if (result.Properties["mail"].Count > 0)
                {
                    email = result.Properties["mail"][0].ToString();
                    emailDictionary[userName] = email;
                    return email;
                }
            }

            email = userName + "@" + cfg.emaildomain;
            Console.Error.WriteLine("Failed ldap lookup for {0}. Using {1}.", userName, email);
            return email;
        }

        /// <summary>
        /// Tests if the current configuration works by sending email.
        /// </summary>
        /// <param name="email"> Who to send email to. </param>
        private static void TestSetup(string email)
        {
            Console.WriteLine("Testing notifier setup.");
            Console.Write("1. Attempting to load configuration... ");
            Configuration cfg = Configuration.Load();
            if (cfg == null)
            {
                Console.WriteLine("Failure!");
                Console.WriteLine("Run 'Notifier help' for help with configuring this program.");
                return;
            }
            Console.WriteLine("Success!");
            Console.Write("2. Attempting to connect to the database... ");

            BlackSquareDataContext context = new BlackSquareDataContext(cfg.database);
            int openReviews = (from rr in context.DocumentSets where rr.Status == 0 select rr).Count();
            int totalReviews = (from rr in context.DocumentSets select rr).Count();
            int files = (from ff in context.Documents select ff).Count();

            Console.WriteLine("Success!");

            string mailbody = "If you are reading this message, the notifier configuration should be correct.\n\n" +
                "Check that the mail has come from the right account, and that the following stats are reasonable:\n" +
                "    Open reviews: " + openReviews + "\n    Total reviews: " + totalReviews +
                "\n    Total files in all reviews: " + files + 
                "\n\nRespectfully,\n    Your friendly review notifier.\n";

            bool result = false;
            if (cfg.emailservice != null)
            {
                Console.Write("3. Sending mail using Exchange protocol... ");
                List<MessageType> mail = new List<MessageType>();
                mail.Add(MakeExchangeMessage(email + "@" + cfg.emaildomain,
                    cfg.fromemail == null ? null : cfg.fromemail + "@" + cfg.emaildomain,
                    email + "@" + cfg.emaildomain,
                    "A test email from BlackSquare notifier - sent via Exchange transport", mailbody));
                result = SendExchangeMail(cfg, mail);
            }
            else if (cfg.smtpserver != null)
            {
                Console.Write("3. Sending mail using SMTP protocol... ");
                List<MailMessage> mail = new List<MailMessage>();
                mail.Add(MakeSmtpMessage(email + "@" + cfg.emaildomain,
                    cfg.fromemail == null ? null : cfg.fromemail + "@" + cfg.emaildomain,
                    ResolveUser(cfg, cfg.user), ResolveUser(cfg, cfg.user),
                    "A test email from BlackSquare notifier - sent via SMTP transport", mailbody));
                result = SendSmtpMail(cfg, mail);
            }
            else
            {
                // This should really never happen.
                Console.WriteLine("Failure: mail transport is not configured!");
            }

            if (result)
            {
                Console.WriteLine("Success! (or so we think: check your inbox!");
                Console.WriteLine("Email was sent to {0} @ {1}", email, cfg.emaildomain);
            }
        }

        /// <summary>
        /// Does all the work.
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            if (args.Contains<string>("help") || args.Contains("-h") || args.Contains("-?"))
            {
                DisplayUsage();
                return;
            }

            if (args.Length == 2 && args[0].Equals("testsetup", StringComparison.InvariantCultureIgnoreCase))
            {
                TestSetup(args[1]);
                return;
            }

            int remindDaysOld = -1;
            if (args.Length == 2 && args[0].Equals("--remind", StringComparison.InvariantCultureIgnoreCase))
            {
                if ((!Int32.TryParse(args[1], out remindDaysOld)) || (remindDaysOld <= 0))
                {
                    DisplayUsage();
                    return;
                }
            }

            if (args.Length > 0 && remindDaysOld <= 0)
            {
                SetConfiguration(args);
                return;
            }

            Configuration cfg = Configuration.Load();
            if (cfg == null)
                return;

            Console.WriteLine("Started processing document review reminders @ {0}", DateTime.Now);

            MailTemplates templates = new MailTemplates();

            BlackSquareDataContext context = new BlackSquareDataContext(cfg.database);

            List<MessageType> exchangeItems = cfg.emailservice == null ? null : new List<MessageType>();
            List<MailMessage> smtpItems = cfg.smtpserver == null ? null : new List<MailMessage>();

            var reviewRequests = from er in context.EmailRequests
                                 join rv in context.Reviewers on er.ReviewerId equals rv.Id
                                 join ds in context.DocumentSets on rv.DocumentSetId equals ds.Id
                                 select new
                                 {
                                     er,
                                     rv.UserAlias,
                                     ds
                                 };

            foreach (var request in reviewRequests)
            {
                Console.WriteLine("Sending new review request for {0} to {1}", request.ds.Title, request.UserAlias);

                string subject = "Review for " + request.ds.Title + " by " + request.ds.UserAlias;
                string email = ResolveUser(cfg, request.UserAlias);
                string replyToAlias = ResolveUser(cfg, request.ds.UserAlias);
                string sender = ResolveUser(cfg, cfg.user);
                string from = cfg.fromemail == null ? null : cfg.fromemail + "@" + cfg.emaildomain;
                string body = templates.CreateMail(MailTemplates.MailType.Request, request.ds.Id,
                    request.UserAlias, request.ds.UserAlias, cfg.webserver, Configuration.WEBROOT,
                    null, request.ds.Description);

                if (exchangeItems != null)
                    exchangeItems.Add(MakeExchangeMessage(email, from, replyToAlias, subject, body));

                if (smtpItems != null)
                    smtpItems.Add(MakeSmtpMessage(email, from, replyToAlias, sender, subject, body));

                context.EmailRequests.DeleteOnSubmit(request.er);
            }

            var reviewResponses = from er in context.EmailRequests
                                  join rr in context.Reviews on er.ReviewId equals rr.Id
                                  join ds in context.DocumentSets on rr.DocumentSetId equals ds.Id
                                  select new
                                  {
                                      er,
                                      rr,
                                      ds
                                  };

            foreach (var response in reviewResponses)
            {
                Console.WriteLine("Sending response notification for {0}", response.ds.Title);

                string subject = "Review for " + response.ds.Title + " by " + response.ds.UserAlias;
                string from = cfg.fromemail == null ? null : cfg.fromemail + "@" + cfg.emaildomain;
                string replyToAlias = ResolveUser(cfg, response.rr.UserAlias);
                string sender = ResolveUser(cfg, cfg.user);

                List<string> to = new List<string>();
                List<string> cc = new List<string>();

                string body = null;
                if (response.ds.UserAlias.Equals(response.rr.UserAlias, StringComparison.InvariantCultureIgnoreCase))
                {
                    // This is a response to review comments. The reviewers are on the 'to' line, reviewee is on 'cc'.
                    cc.Add(ResolveUser(cfg, response.ds.UserAlias));

                    foreach (Reviewer r in response.ds.Reviewers)
                        to.Add(ResolveUser(cfg, r.UserAlias));

                    body = templates.CreateMail(MailTemplates.MailType.Response, response.ds.Id, null,
                        response.rr.UserAlias, cfg.webserver, Configuration.WEBROOT, null, response.rr.Comment);
                }
                else
                {
                    // This is a review. The reviewee is on the 'to' line, the reviewers are on 'cc'.
                    to.Add(ResolveUser(cfg, response.ds.UserAlias));

                    foreach (Reviewer r in response.ds.Reviewers)
                        cc.Add(ResolveUser(cfg, r.UserAlias));
                    body = templates.CreateMail(MailTemplates.MailType.Iteration, response.ds.Id, response.rr.UserAlias,
                        response.ds.UserAlias, cfg.webserver, Configuration.WEBROOT,
                        ReviewStatusToSentence(response.rr.Verdict), response.rr.Comment);
                }

                if (to.Count > 0)
                {
                    if (exchangeItems != null)
                        exchangeItems.Add(MakeExchangeMessage(to, cc, from, replyToAlias, subject, body.ToString()));

                    if (smtpItems != null)
                        smtpItems.Add(MakeSmtpMessage(to, cc, from, replyToAlias, sender, subject, body.ToString()));
                }

                context.EmailRequests.DeleteOnSubmit(response.er);
            }

            if (remindDaysOld > 0)
            {
                DateTime threshold = DateTime.Now.AddDays(-remindDaysOld);
                var oldDocSetQuery = from rr in context.DocumentSets
                                     where rr.Status == 0 && rr.TimeStamp < threshold
                                     select rr;

                foreach (DocumentSet ds in oldDocSetQuery)
                {
                    Console.WriteLine("Sending review reminder for {0}", ds.Title);

                    string subject = "Review for " + ds.Title + " by " + ds.UserAlias;
                    string email = ResolveUser(cfg, ds.UserAlias);
                    string sender = ResolveUser(cfg, cfg.user);
                    string from = cfg.fromemail == null ? null : cfg.fromemail + "@" + cfg.emaildomain;
                    string body = templates.CreateMail(MailTemplates.MailType.Reminder, ds.Id,
                        null, ds.UserAlias, cfg.webserver, Configuration.WEBROOT,
                        null, ds.Description);

                    if (exchangeItems != null)
                        exchangeItems.Add(MakeExchangeMessage(email, from, null, subject, body));

                    if (smtpItems != null)
                        smtpItems.Add(MakeSmtpMessage(email, from, null, sender, subject, body));
                }
            }

            if (exchangeItems != null && exchangeItems.Count() > 0)
                SendExchangeMail(cfg, exchangeItems);
            if (smtpItems != null && smtpItems.Count() > 0)
                SendSmtpMail(cfg, smtpItems);

            context.SubmitChanges();

            Console.WriteLine("Finished processing review mail @ {0}", DateTime.Now);
        }
    }
}
